/*
 *
 *  Connection Manager
 *
 *  Copyright (C) 2007-2009  Intel Corporation. All rights reserved.
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License version 2 as
 *  published by the Free Software Foundation.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 *
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include <stdio.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <sys/stat.h>

#include "connman.h"

#define	_DBG_RESOLVER(fmt, arg...)	DBG(DBG_RESOLV, fmt, ## arg)

static GSList *entry_list = NULL;
static GSList *resolver_list = NULL;

static void reclaim_entry(struct connman_resolver_state *entry)
{
	struct connman_resolver *resolver = entry->resolver;

	/*
	 * NB: resolver may be NULL if the plugin was unregister'd
	 */
	if (resolver != NULL && resolver->unset != NULL)
		resolver->unset(entry);

	entry_list = g_slist_remove(entry_list, entry);
	g_strfreev(entry->servers);
	g_strfreev(entry->search_domains);
	g_free(entry->domain);
	g_free(entry->interface);
	g_free(entry);
}

static gint compare_priority(gconstpointer a, gconstpointer b)
{
	const struct connman_resolver *resolver1 = a;
	const struct connman_resolver *resolver2 = b;

	return resolver2->priority - resolver1->priority;
}

/**
 * connman_resolver_register:
 * @resolver: resolver module
 *
 * Register a new resolver module
 *
 * Returns: %0 on success
 */
int connman_resolver_register(struct connman_resolver *resolver)
{
	_DBG_RESOLVER("resolver %p name %s", resolver, resolver->name);

	resolver_list = g_slist_insert_sorted(resolver_list, resolver,
							compare_priority);
	if (resolver->set != NULL) {
		GSList *list;

		/* try to bind existing entries */
		for (list = entry_list; list != NULL; list = list->next) {
			struct connman_resolver_state *entry = list->data;

			if (entry->resolver == NULL && resolver->set(entry) == 0)
				entry->resolver = resolver;
		}
	}
	return 0;
}

/**
 * connman_resolver_unregister:
 * @resolver: resolver module
 *
 * Remove a previously registered resolver module
 */
void connman_resolver_unregister(struct connman_resolver *resolver)
{
	GSList *list;

	_DBG_RESOLVER("resolver %p name %s", resolver, resolver->name);

	resolver_list = g_slist_remove(resolver_list, resolver);
	/*
	 * Unbind existing entries.  We do not reclaim the entries
	 * as that happens when the associated ipconfig records are
	 * torn down (doing it here would leave ipconfig holding
	 * stale references).
	 */
	for (list = entry_list; list != NULL; list = list->next) {
		struct connman_resolver_state *entry = list->data;

		if (entry->resolver == resolver) {
			if (resolver->unset != NULL)
				resolver->unset(entry);
			entry->resolver = NULL;
		}
	}
}

/**
 * connman_resolver_set:
 * @interface: network interface
 * @domain: local domain name
 * @search_domains: names for searching !FQDN's
 * @servers: server addresses
 *
 * Add resolver state to the current list
 */
struct connman_resolver_state *connman_resolver_set(const char *interface,
	const char *domain, char **search_domains, char **servers)
{
	struct connman_resolver_state *entry;
	char **serverp;
	GSList *list;

	if (servers == NULL) {
		_DBG_RESOLVER("No DNS servers for domain %s interface %s",
		    domain, interface);
		return NULL;
	}

	connman_info("Add DNS servers for domain %s interface %s:",
	    domain, interface);

	for (serverp = servers; *serverp != NULL; serverp++)
		connman_info(" server %s", *serverp);

	entry = g_try_new0(struct connman_resolver_state, 1);
	if (entry == NULL) {
		connman_error("%s: no memory for resolver state", __func__);
		return NULL;
	}
	entry->interface = g_strdup(interface);
	entry->domain = g_strdup(domain);
	entry->search_domains = g_strdupv(search_domains);
	entry->servers = g_strdupv(servers);

	entry_list = g_slist_append(entry_list, entry);

	for (list = resolver_list; list; list = list->next) {
		struct connman_resolver *resolver = list->data;

		if (resolver->set == NULL)
			continue;

		if (resolver->set(entry) == 0) {
			entry->resolver = resolver;
			break;
		}
	}
	return entry;
}

static int
g_strv_equal(char **a, char **b)
{
	int i;

	if (a == NULL && b == NULL)
		return TRUE;
	if (a == NULL || b == NULL)
		return FALSE;
	/* NB: a and b != NULL */
	for (i = 0; a[i] != NULL; i++) {
		if (b[i] == NULL)
			return FALSE;
		if (g_strcmp0(a[i], b[i]) != 0)
			return FALSE;
	}
	return TRUE;
}

/**
 * connman_resolver_update:
 * @interface: network interface
 * @domain: local domain name
 * @search_domains: names for searching !FQDN's
 * @servers: server addresses
 *
 * Update state for a previously created resolver entry.
 */
struct connman_resolver_state *connman_resolver_update(
	struct connman_resolver_state *entry,
	const char *domain, char **search_domains, char **servers)
{
	char **serverp;
	int changed;

	if (servers == NULL) {
		connman_resolver_unset(entry);
		return NULL;
	}

	connman_info("Update DNS servers for domain %s interface %s:",
	    domain, entry->interface);

	for (serverp = servers; *serverp != NULL; serverp++)
		connman_info(" server %s", *serverp);

	changed = FALSE;
	if (g_strcmp0(entry->domain, domain) != 0) {
		g_free(entry->domain);
		entry->domain = g_strdup(domain);
		changed = TRUE;
	}
	if (g_strv_equal(search_domains, entry->search_domains) == FALSE) {
		g_strfreev(entry->search_domains);
		entry->search_domains = g_strdupv(search_domains);
		changed = TRUE;
	}
	if (g_strv_equal(servers, entry->servers) == FALSE) {
		g_strfreev(entry->servers);
		entry->servers = g_strdupv(servers);
		changed = TRUE;
	}

	if (changed == TRUE && entry->resolver != NULL) {
		struct connman_resolver *resolver = entry->resolver;

		resolver->unset(entry);
		if (resolver->set(entry) != 0)
			entry->resolver = NULL;
	}
	return entry;
}

static int interface_compare(gconstpointer a, gconstpointer b)
{
	const struct connman_resolver_state *entry =
		(const struct connman_resolver_state *)a;
	const char *interface = (const char *)b;

	return g_strcmp0(entry->interface, interface);
}

/**
 * connman_resolver_lookup:
 * @interface: network interface
 *
 * Find the resolver state for a given interface
 */
const struct connman_resolver_state *connman_resolver_lookup(
	const char *interface)
{
	GSList *list;

	list = g_slist_find_custom(entry_list, interface, interface_compare);
	if (list != NULL)
		return (const struct connman_resolver_state *) list->data;
	return NULL;
}

/**
 * connman_resolver_unset:
 * @interface: network interface
 *
 * Remove all resolver server address for the specified interface
 */
void connman_resolver_unset(struct connman_resolver_state *entry)
{
	_DBG_RESOLVER("Remove DNS servers for domain %s interface %s",
	    entry->domain, entry->interface);
	reclaim_entry(entry);
}

int connman_resolvfile_write(const char *filename, const char *domain,
			     char **search_domains, char **servers)
{
	mode_t omask;
	int fd, err, ret;
	char *cmd;
	char *search_list;
	char **serverp;
        /*
         * Send queries one-at-a-time, rather than parallelizing IPv4
         * and IPv6 queries for a single host.  Use minimum 1-second
	 * request timeout.
         */
        const char extra_options[] = "options single-request timeout:1\n";

	_DBG_RESOLVER("write file %s,  domain %s search_domains %p servers %p",
	    filename, domain, search_domains, servers);

	omask = umask(022);
	err = 0;
	fd = open(filename, O_RDWR | O_CREAT | O_TRUNC,
	    S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
	if (fd < 0) {
		connman_error("%s: open failed: %s", __func__,
		    strerror(errno));
		err = -errno;
		goto done;
	}

	cmd = "# Generated by Connection Manager\n";
	ret = write(fd, cmd, strlen(cmd));
	if (ret == -1)
		goto badwrite;

	if (search_domains != NULL)
		search_list = g_strjoinv(" ", search_domains);
	else if (domain != NULL)
		search_list = g_strdup_printf("%s.", domain);
	else
		search_list = NULL;

	if (search_list != NULL) {
		cmd = g_strdup_printf("search %s\n", search_list);
		g_free(search_list);
		if (cmd == NULL) {
			connman_error("%s: strdup failed", __func__);
			err = -ENOMEM;
			goto done;
		}

		ret = write(fd, cmd, strlen(cmd));
		g_free(cmd);
		if (ret == -1)
			goto badwrite;
	}

	for (serverp = servers; *serverp != NULL; serverp++) {
		cmd = g_strdup_printf("nameserver %s\n", *serverp);
		ret = write(fd, cmd, strlen(cmd));
		g_free(cmd);
		if (ret == -1)
			goto badwrite;
	}

        ret = write(fd, extra_options, strlen(extra_options));
        if (ret == -1)
                goto badwrite;

done:
	if (fd >= 0)
		close(fd);
	(void) umask(omask);
	return err;
badwrite:
	connman_error("%s: write failed: %s (len %zu)",
	    __func__, strerror(errno), strlen(cmd));
	err = -errno;
	goto done;
}

static int resolvfile_set(const struct connman_resolver_state *entry)
{
	_DBG_RESOLVER("interface %s domain %s", entry->interface,
	    entry->domain);

	return connman_resolvfile_write("/etc/resolv.conf", entry->domain,
	    entry->search_domains, entry->servers);
}

static int resolvfile_unset(const struct connman_resolver_state *entry)
{
	_DBG_RESOLVER("interface %s", entry->interface);

	return 0;
}

static struct connman_resolver resolvfile_resolver = {
	.name		= "resolvfile",
	.priority	= CONNMAN_RESOLVER_PRIORITY_LOW,
	.set		= resolvfile_set,
	.unset		= resolvfile_unset,
};

int __connman_resolver_init(void)
{
	return connman_resolver_register(&resolvfile_resolver);
}

void __connman_resolver_cleanup(void)
{
	connman_resolver_unregister(&resolvfile_resolver);
}
